07. 논리 함수¶
7.1 조건 검사¶
벡터에 조건을 적용하면 원소 개수만큼의 논리 벡터가 만들어집니다. 그런데 if() 조건문처럼 "그래서 결론이 뭔데?"라는 질문에는 길이가 1인 답, 즉 TRUE 또는 FALSE 하나가 필요합니다. 예를 들어 "이 반에 60점 미만인 학생이 한 명이라도 있는가?"와 "이 반 학생 전원이 60점 이상인가?"는 각각 다른 방식으로 논리 벡터를 요약해야 답할 수 있는 질문입니다.
조건을 적용하면 학생별 결과(논리 벡터)까지만 얻을 수 있고, "한 명이라도"·"전원"에 대한 답은 별도로 요약해야 합니다.
any()는 "논리 벡터 중 하나라도 TRUE인가"를, all()은 "논리 벡터 모두가 TRUE인가"를 답하는 함수입니다. 각각 OR과 AND를 벡터 전체로 확장한 것으로 볼 수 있습니다.
any()¶
any(..., na.rm = FALSE)는 주어진 논리 벡터 중 어느 하나라도 TRUE이면 TRUE를 반환합니다.
인자
- ...: 검사할 논리 벡터입니다. 콤마로 여러 개를 나열할 수 있으며, 이 경우 모두 하나로 이어붙인 것처럼 취급해 그중 하나라도 TRUE가 있는지 검사합니다(...은 여러 값을 한꺼번에 받는 R의 특수 인자로, 자세한 문법은 16.2절에서 다룹니다).
- na.rm: 결측치(NA)를 계산에서 제외할지 여부를 지정합니다. 기본값은 FALSE이며, 이 경우 NA는 아래 주의사항처럼 특별하게 처리됩니다. TRUE로 지정하면 NA를 먼저 제거한 뒤 나머지 값만으로 판단합니다.
(x <- seq(-3, 3, by = 1))
#> [1] -3 -2 -1 0 1 2 3
any(x < 0)
#> [1] TRUE
if (any(x < 0)) cat("x는 음수를 포함하고 있습니다\n")
#> x는 음수를 포함하고 있습니다
여러 조건을 한 번에 검사할 수도 있습니다. ...에 조건을 콤마로 나열하면 됩니다.
all()¶
all(..., na.rm = FALSE)는 주어진 논리 벡터 모두가 TRUE이면 TRUE를 반환합니다. 어느 하나라도 FALSE가 있으면 FALSE를 반환합니다.
인자
- ...: 검사할 논리 벡터입니다. any()와 마찬가지로 여러 개를 콤마로 나열하면 모두 이어붙여 전원 TRUE인지 검사합니다.
- na.rm: NA를 제외할지 여부입니다. 기본값 FALSE에서의 동작은 아래 주의사항을 참고하십시오.
(x <- seq(-3, 3, by = 1))
#> [1] -3 -2 -1 0 1 2 3
all(x < 0)
#> [1] FALSE
if (all(x >= -3)) cat("x에 포함된 수는 모두 -3 이상입니다\n")
#> x에 포함된 수는 모두 -3 이상입니다
주의: NA가 섞여 있을 때의
any()·all()
na.rm = FALSE(기본값) 상태에서any()와all()은 NA를 "모른다"는 뜻 그대로 취급합니다. 이미 확정된 결과(TRUE 하나 또는 FALSE 하나)가 있으면 NA와 무관하게 그 값을 반환하지만, 확정된 결과가 없는 상태에서 NA만 섞여 있으면 결과도 NA가 됩니다.any(c(FALSE, NA)) # TRUE가 없고 NA만 있음 -> 확정 불가 any(c(FALSE, NA), na.rm = TRUE) # na.rm = TRUE: NA를 제거하면 FALSE만 남음 -> 확정 any(c(TRUE, NA)) # 이미 TRUE가 있으므로 NA와 무관하게 확정 #> [1] NA #> [1] FALSE #> [1] TRUE all(c(TRUE, NA)) # FALSE가 없고 NA만 있음 -> 확정 불가 all(c(TRUE, NA), na.rm = TRUE) # na.rm = TRUE: NA를 제거하면 TRUE만 남음 -> 확정 all(c(FALSE, NA)) # 이미 FALSE가 있으므로 NA와 무관하게 확정 #> [1] NA #> [1] TRUE #> [1] FALSE결측치가 섞인 데이터에서는
na.rm인자를 빠뜨리면 조건문이 예상과 다르게 NA를 반환해 오류로 이어지기 쉬우므로, 결측치 가능성이 있는 데이터에는na.rm = TRUE를 명시적으로 지정하는 습관을 들이는 것이 안전합니다.
isTRUE() / isFALSE()¶
isTRUE(x)는 x가 "길이 1의 TRUE"인 경우에만 TRUE를 반환합니다. isFALSE(x)는 반대로 x가 "길이 1의 FALSE"인 경우에만 TRUE를 반환합니다.
인자
- x: 검사할 대상입니다. 논리값뿐 아니라 숫자·문자·벡터·NA·NULL 등 R의 어떤 객체든 넣을 수 있습니다. isTRUE()·isFALSE()는 x가 정확히 "길이 1의 TRUE(또는 FALSE)"가 아니면 오류를 내는 대신 항상 안전하게 FALSE를 반환하도록 설계되어 있습니다.
if() 조건에는 길이 1인 논리값만 들어갈 수 있습니다. 그런데 x == TRUE 같은 비교는 x가 벡터이거나 NA를 포함하면 길이가 1이 아니거나 NA를 반환할 수 있어, 그대로 if()에 넣으면 오류가 나거나 예상 밖의 결과가 나올 위험이 있습니다.
x <- c(TRUE, TRUE)
x == TRUE # 벡터 그대로 비교되어 길이 2가 됨
#> [1] TRUE TRUE
isTRUE(x) # 길이가 1이 아니므로 안전하게 FALSE로 판정
#> [1] FALSE
y <- NA
y == TRUE # NA
#> [1] NA
isTRUE(y) # NA조차 안전하게 FALSE로 판정
#> [1] FALSE
isTRUE()·isFALSE()는 이처럼 "정확히 TRUE(또는 FALSE) 하나인가"만 판정하고, 그 밖의 모든 경우(벡터, NA, 문자열 "TRUE" 등)에는 안전하게 FALSE를 반환하므로 함수 인자 검증이나 조건문 방어 코드에 즐겨 쓰입니다.
check_option <- function(verbose = FALSE) {
if (isTRUE(verbose)) cat("자세히 출력합니다\n") else cat("간단히 출력합니다\n")
}
check_option(verbose = "yes") # 잘못된 값이 들어와도 오류 없이 안전하게 처리됨
#> 간단히 출력합니다
identical() / all.equal()¶
부동소수점(floating point) 연산은 IEEE 754 표준의 특성상, 수학적으로는 같아야 할 값이 컴퓨터 내부에서는 미세하게 다르게 저장될 수 있습니다. 이 때문에 == 비교가 직관과 다르게 FALSE를 반환하는 일이 자주 벌어집니다.
identical()은 두 객체가 값·자료형·속성까지 정확히 일치하는지 엄격하게 검사하고, all.equal()은 부동소수점 오차 수준의 미세한 차이는 "같다"고 봐줍니다.
identical(0.1 + 0.2, 0.3) # 저장된 이진수 값까지 완전히 같아야 함
#> [1] FALSE
all.equal(0.1 + 0.2, 0.3) # 오차 허용범위 안이면 TRUE
#> [1] TRUE
인자
- identical(x, y, ...)의 x, y는 비교할 두 객체입니다. 값뿐 아니라 자료형(typeof())과 속성(attributes())까지 하나라도 다르면 FALSE가 됩니다. 이 밖에도 num.eq(숫자 비교 방식), attrib.as.set(속성 순서를 무시할지) 등 비교 기준을 세밀하게 조정하는 인자가 더 있지만, 기본값을 그대로 쓰는 것으로 충분한 경우가 대부분입니다.
- all.equal(target, current, tolerance = ..., ...)의 target·current는 비교할 두 값이고, tolerance는 "이 정도 차이는 같다고 봐줄" 허용오차입니다. 기본값은 sqrt(.Machine$double.eps)(약 1.49e-08)로, 부동소수점 계산에서 흔히 생기는 미세한 오차를 넉넉히 덮는 크기입니다. tolerance를 더 작게 지정하면 더 엄격하게, 더 크게 지정하면 더 너그럽게 비교합니다.
identical(x, y)는 자료형이 다르면 값이 같아 보여도 FALSE를 반환할 만큼 엄격합니다. 반면 all.equal()은 값이 다를 경우 TRUE/FALSE가 아니라 차이를 설명하는 문자열을 반환할 수 있다는 점이 특이합니다. 그래서 if() 조건에 바로 쓰면 위험하며, 반드시 isTRUE()로 감싸 "정말로 같다고 판정됐는가"만 확인하는 것이 관례입니다.
identical(1, 1L) # 실수(double)와 정수(integer)는 자료형이 달라 FALSE
#> [1] FALSE
identical(NA, NA_integer_) # NA(logical)와 NA_integer_(integer)도 자료형이 달라 FALSE
#> [1] FALSE
all.equal(1, 1.1) # 값이 다르면 문자열을 반환 -- 논리값이 아님에 주의
#> [1] "Mean relative difference: 0.1"
isTRUE(all.equal(1, 1.1)) # if()에 안전하게 쓰려면 반드시 isTRUE()로 감싸야 함
#> [1] FALSE
허용오차를 직접 조정할 수도 있습니다.
두 실수(벡터)가 부동소수점 오차를 감안하고도 같은지 판정할 때는 x == y나 identical(x, y)가 아니라 isTRUE(all.equal(x, y)) 관용구를 사용하는 것이 R의 표준적인 방식입니다.
7.2 위치 탐색¶
논리 벡터에 which()를 적용하면 TRUE인 위치의 인덱스만 뽑아낼 수 있고, 이를 다시 원래 벡터의 인덱싱에 활용할 수 있습니다.
scores <- c(85, 55, 90, 40, 70)
which(scores < 60) # 조건을 만족하는 위치(인덱스)
#> [1] 2 4
scores[which(scores < 60)] # 위치를 이용해 실제 값 조회
#> [1] 55 40
which()가 조건을 만족하는 모든 위치를 찾는 일반적인 도구라면, which.min()·which.max()는 "최솟값·최댓값의 위치"라는 자주 쓰이는 특수한 경우를 더 간단하고 빠르게 처리하는 전용 함수입니다.
which()¶
which(x, arr.ind = FALSE, useNames = TRUE)는 벡터(또는 배열) x에서 TRUE인 것의 위치를 반환합니다. x가 배열인 경우 arr.ind = TRUE이면 배열 형태의 위치를 반환합니다.
인자
- x: 위치를 찾을 논리 벡터 또는 논리형 배열(행렬 포함)입니다. 주의할 점은 which()가 x를 논리형으로 자동 변환해주지 않는다는 것입니다. 대부분의 R 함수와 달리, x에 논리형이 아닌 값(숫자·문자 등)을 그대로 넣으면 오류가 납니다. 그래서 which(1:5)처럼 쓰는 것이 아니라 which(x > 0)처럼 항상 비교·조건 연산의 결과(논리 벡터)를 넣어야 합니다.
- arr.ind: x가 배열(또는 행렬)일 때, 결과를 "전체를 한 줄로 늘어놓은 위치"(기본값 FALSE) 대신 "행·열(·층) 좌표" 형태로 받을지 지정합니다. TRUE로 지정하면 각 행이 하나의 원소 좌표를 나타내는 행렬을 반환합니다.
- useNames: arr.ind = TRUE이고 x에 dimnames(행 이름·열 이름)가 지정되어 있을 때, 그 이름들을 결과 행렬에도 그대로 붙일지 여부입니다. 기본값 TRUE는 이름을 붙이고, FALSE는 이름 없이 순번([1,], [2,] 등)만 붙입니다. x가 단순 벡터이거나 arr.ind = FALSE인 경우에는 이 인자가 결과에 영향을 주지 않습니다.
# 배열 위치 반환
x <- 5:16
dim(x) <- c(2, 2, 3); x # x를 2x2 행렬의 3층 배열로 만들기
#> , , 1
#>
#> [,1] [,2]
#> [1,] 5 7
#> [2,] 6 8
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] 9 11
#> [2,] 10 12
#>
#> , , 3
#>
#> [,1] [,2]
#> [1,] 13 15
#> [2,] 14 16
#>
which(x == 15) # 벡터형태의 위치 반환
#> [1] 11
which(x == 15, arr.ind = TRUE) # 배열형태의 위치 반환
#> dim1 dim2 dim3
#> [1,] 1 2 3
# 행렬 위치 반환 (배열과 유사)
x <- matrix(5:16, 3, 4); x
#> [,1] [,2] [,3] [,4]
#> [1,] 5 8 11 14
#> [2,] 6 9 12 15
#> [3,] 7 10 13 16
which(x %% 3 == 0) # 벡터형태의 위치 반환
#> [1] 2 5 8 11
which(x %% 3 == 0, arr.ind = TRUE) # 행렬형태의 위치 반환
#> row col
#> [1,] 2 1
#> [2,] 2 2
#> [3,] 2 3
#> [4,] 2 4
useNames의 효과는 행렬에 이름(dimnames)이 붙어 있을 때 비로소 드러납니다.
m <- matrix(1:6, 2, 3, dimnames = list(c("r1", "r2"), c("c1", "c2", "c3")))
m
#> c1 c2 c3
#> r1 1 3 5
#> r2 2 4 6
which(m > 3, arr.ind = TRUE) # useNames 기본값 TRUE -> 행 이름이 붙음
#> row col
#> r2 2 2
#> r1 1 3
#> r2 2 3
which(m > 3, arr.ind = TRUE, useNames = FALSE) # 이름 없이 순번만 표시
#> [,1] [,2]
#> [1,] 2 2
#> [2,] 1 3
#> [3,] 2 3
# 데이터 프레임 위치 반환
str(women) # R 내장 데이터셋 women - 미국 여성 키(인치)와 몸무게(파운드)
#> 'data.frame': 15 obs. of 2 variables:
#> $ height: num 58 59 60 61 62 63 64 65 66 67 ...
#> $ weight: num 115 117 120 123 126 129 132 135 139 142 ...
which(women$height < 60)
#> [1] 1 2
women[which(women$height < 60), ]
#> height weight
#> 1 58 115
#> 2 59 117
which.min()¶
which.min(x)는 벡터 x의 최솟값 위치를 반환합니다.
인자
- x: 위치를 찾을 숫자형(또는 순서를 비교할 수 있는) 벡터입니다. any()·all()과 달리 na.rm 인자가 따로 없는데, 이는 which.min()·which.max()가 애초에 NA를 항상 무시하고 나머지 값 중에서만 최솟값·최댓값 위치를 찾도록 설계되어 있기 때문입니다.
NA가 섞여 있어도 별도 설정 없이 자동으로 무시됩니다.
# 데이터프레임
str(women)
#> 'data.frame': 15 obs. of 2 variables:
#> $ height: num 58 59 60 61 62 63 64 65 66 67 ...
#> $ weight: num 115 117 120 123 126 129 132 135 139 142 ...
which.min(women$weight)
#> [1] 1
women[which.min(women$weight), ]
#> height weight
#> 1 58 115
which.max()¶
which.max(x)는 벡터 x의 최댓값 위치를 반환합니다. 인자 x의 의미와 NA 처리 방식은 which.min()과 동일합니다.
# 데이터프레임
str(women)
#> 'data.frame': 15 obs. of 2 variables:
#> $ height: num 58 59 60 61 62 63 64 65 66 67 ...
#> $ weight: num 115 117 120 123 126 129 132 135 139 142 ...
which.max(women$weight)
#> [1] 15
women[which.max(women$weight), ]
#> height weight
#> 15 72 164
주의:
which.min()·which.max()는 동점(tie)이 여러 개 있어도 가장 먼저 나오는 위치 하나만 반환합니다. 최솟값(또는 최댓값)을 가진 위치를 모두 찾으려면which(x == min(x))처럼which()와min()(또는max())을 조합해야 합니다.
7.3 비트 논리 연산 함수¶
색상 코드(RGB)나 파일 접근 권한(읽기·쓰기·실행)처럼, 여러 개의 켜짐/꺼짐(On/Off) 정보를 정수 하나의 비트(bit) 각각에 압축해서 저장하고 다시 꺼내야 하는 경우가 있습니다.
유닉스 계열 파일 권한은 읽기(4) · 쓰기(2) · 실행(1)을 각각 하나의 비트로 표현하고, 이를 더해 하나의 숫자(예: 6 = 읽기+쓰기)로 압축합니다. 이런 압축과 해제를 R에서 하려면 일반적인 &, | 대신 정수의 비트 단위를 다루는 전용 함수가 필요합니다.
R은 정수형(integer) 값의 각 비트에 대해 AND·OR·XOR·NOT과 자리이동(shift) 연산을 수행하는 bitw*() 계열 함수를 제공합니다. bitwAnd(a, b), bitwOr(a, b), bitwXor(a, b), bitwNot(a), bitwShiftL(a, n), bitwShiftR(a, n)은 모두 정수(또는 정수로 강제 변환 가능한 값)를 입력받아 비트 단위 연산 결과를 정수로 반환합니다.
bitwAnd() / bitwOr() / bitwXor() / bitwNot()¶
bitwAnd(a, b)는 두 정수의 비트 단위 AND를, bitwOr(a, b)는 비트 단위 OR을, bitwXor(a, b)는 비트 단위 XOR(둘 중 하나만 1일 때 1)을, bitwNot(a)는 비트 단위 NOT(부정)을 반환합니다.
인자
- a, b: 비트 연산을 적용할 정수(integer)입니다. 32비트 정수로 취급되며, 정수가 아닌 실수를 넣으면 자동으로 정수로 변환됩니다. 벡터를 넣으면 원소별로 연산되며, 길이가 다르면 짧은 쪽이 재사용(recycling)됩니다.
- bitwNot()은 비교 대상이 없는 단항 연산이라 a 하나만 받습니다.
# 파일 권한 예시: 읽기=4(100), 쓰기=2(010), 실행=1(001)
READ <- 4L; WRITE <- 2L; EXECUTE <- 1L
# 읽기+쓰기 권한을 하나의 정수로 압축
perm <- bitwOr(READ, WRITE)
perm
#> [1] 6
# perm에 특정 권한이 포함되어 있는지 검사 (0이 아니면 포함된 것)
bitwAnd(perm, READ) # 읽기 권한 포함 여부
#> [1] 4
bitwAnd(perm, EXECUTE) # 실행 권한 포함 여부
#> [1] 0
a(또는 b)에 벡터를 넣으면 원소마다 연산이 적용됩니다.
주의:
bitwNot(a)는 정수를 32비트 부호있는 정수(2의 보수)로 취급해 모든 비트를 뒤집습니다. 따라서bitwNot(0L)은 직관적으로 예상하기 쉬운 큰 양수가 아니라-1을 반환합니다.
bitwShiftL() / bitwShiftR()¶
bitwShiftL(a, n)은 정수 a의 비트를 왼쪽으로 n칸 이동(2를 n번 곱하는 것과 동일)시키고, bitwShiftR(a, n)은 오른쪽으로 n칸 이동(2를 n번 나누는 것과 동일)시킵니다. 플래그 상수를 만들 때 자주 쓰입니다.
인자
- a: 이동시킬 정수입니다.
- n: 이동할 비트 칸 수입니다. 0 이상 31 이하의 정수여야 하며, 이 범위를 벗어나면(예: 32 이상, 음수) 오류 없이 조용히 NA를 반환하므로 결과가 뜻밖에 NA로 나온다면 n 값의 범위부터 확인해 보아야 합니다. 또한 bitwShiftL()은 n이 정상 범위에 있더라도 결과가 32비트 signed integer의 표현 범위를 벗어나면 NA를 반환할 수 있습니다(예: bitwShiftL(1L, 31)).